﻿/*
Copyright (c) 2012 Maksim Gordeev

Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or
substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

 */


/*
 * Сделано в SharpDevelop.
 * Пользователь: maxxadm
 * Дата: 25.07.2012
 * Время: 16:01
 * 
 * Для изменения этого шаблона используйте Сервис | Настройка | Кодирование | Правка стандартных заголовков.
 */
using System;
using System.Threading;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.ComponentModel;
using System.IO;
using Hammock;
using Hammock.Web;
using Newtonsoft.Json;

namespace jellybird.core
{
	/// <summary>
	/// Description of TwTimeline.
	/// </summary>
	public sealed class TwTimeline:TwObjectSortedList<TwTweet>
	{
		#region singleton logic
		public static TwTimeline Get(TwCredentials credentials,TimelineKind kind)
		{
			Credentials_kind key = new Credentials_kind()
			{
				Credentials = credentials,
				Kind = kind
			};

			//Это не очень хорошо
			//потому что делегат будет выполняться
			//без мультипоточной защиты
			return instances.GetOrAdd
				(key,
				 (key_ctor) => new TwTimeline(key_ctor.Credentials, key_ctor.Kind));
		}

		private static ConcurrentDictionary<Credentials_kind, TwTimeline> instances = new ConcurrentDictionary<Credentials_kind, TwTimeline>();

		public static bool Remove(TwCredentials credentials,TimelineKind kind)
		{
			Credentials_kind key = new Credentials_kind()
			{
				Credentials = credentials,
				Kind = kind
			};
			TwTimeline removing=null;
			return instances.TryRemove(key,out removing);
		}
		
		public static void RemoveAll()
		{
			instances.Clear();
		}
		
		private class Credentials_kind:IEquatable<Credentials_kind>
		{
			public TwCredentials Credentials;
			public TimelineKind Kind;

			#region Члены IEquatable<Credentials_kind>

			public bool Equals(Credentials_kind other)
			{
				if (other == null) return false;
				if (!Credentials.Equals(other.Credentials)) return false;
				if (Kind != other.Kind) return false;
				return true;
			}

			#endregion
			
			#region Equals and GetHashCode implementation
			public override bool Equals(object obj)
			{
				TwTimeline.Credentials_kind other = obj as TwTimeline.Credentials_kind;
				if (other == null)
					return false;
				return this.Equals(other);
			}
			
			public override int GetHashCode()
			{
				int hashCode = 0;
				unchecked {
					if (Credentials != null)
						hashCode += 1000000007 * Credentials.GetHashCode();
					hashCode += 1000000009 * Kind.GetHashCode();
				}
				return hashCode;
			}
			
			public static bool operator == (TwTimeline.Credentials_kind lhs, TwTimeline.Credentials_kind rhs)
			{
				if (ReferenceEquals(lhs, rhs))
					return true;
				if (ReferenceEquals(lhs, null) || ReferenceEquals(rhs, null))
					return false;
				return lhs.Equals(rhs);
			}
			
			public static bool operator != (TwTimeline.Credentials_kind lhs, TwTimeline.Credentials_kind rhs)
			{
				return !(lhs == rhs);
			}
			#endregion

		}
		#endregion


		private TwTimeline(System.ComponentModel.ListSortDirection direction)
			:base("Id",direction)
		{}
		
		private TwTimeline(System.ComponentModel.ListSortDirection direction,RestClient client)
			:base(client,"Id",direction)
		{}
		
		private TwTimeline
			(TwCredentials credentials,TimelineKind Kind)
			:base("Id",ListSortDirection.Descending)
		{
			Client=new Hammock.RestClient
				()
			{
				Authority=Constants.API_TWITTER_COM,
				Credentials=new Hammock.Authentication.OAuth.OAuthCredentials()
				{
					ConsumerKey=credentials.ConsumerKey,
					ConsumerSecret=credentials.ConsumerSecret,
					SignatureMethod=Hammock.Authentication.OAuth.OAuthSignatureMethod.HmacSha1,
					Token=credentials.AccessToken,
					TokenSecret=credentials.AccessTokenSecret,
					Type=Hammock.Authentication.OAuth.OAuthType.AccessToken
				},
				Deserializer=new JsonDeserializerWrapper(),
				VersionPath=Constants.API_VERSION_1_1
			};
			Credentials=credentials;
			this.Kind = Kind;
//			this.AutoUpdatePeriod=TimeSpan.FromMinutes(1);
//			this.EnableAutoUpdate=true;
			
			//initial update
			//TODO или загрузить из файла
//			this.BeginUpdate(null,null,null);
		}
		
		protected override string create_store_file_name()
		{
			string ret=
				string.Format
				("{0}_{1}.json",
				 Kind.ToString(),
				 Credentials.AccessToken);
			ret=System.IO.Path.Combine
				(Environment.GetFolderPath
				 (Environment.SpecialFolder.ApplicationData,
				  Environment.SpecialFolderOption.DoNotVerify),
				 "jellybird",
				 ret);
			return ret;
		}
		
		#region readed / unreaded tweets handling
		
		
		private TwTweet m_FirstReadedTweet;
		public TwTweet FirstReadedTweet
		{
			get
			{
				return m_FirstReadedTweet;
			}
			set
			{
				m_FirstReadedTweet=value;
				On_property_changed("FirstReadedTweet");
				On_property_changed("CountOfUnreadedTweets");
				On_property_changed("HasUnreadedTweets");
			}
		}
		
		public int CountOfUnreadedTweets
		{
			get
			{
				if(FirstReadedTweet==null)
					return 0;
				
				return IndexOf(FirstReadedTweet);
			}
		}
		
		public bool HasUnreadedTweets
		{
			get{return CountOfUnreadedTweets!=0;}
		}
		#endregion
		
		#region props (thread safety)
		public bool ClientFlag { get; set; }
		
		private bool m_enable_autoupdate;
		public bool EnableAutoUpdate
		{
			get
			{
				bool ret=false;
				lock(SyncRoot)
				{
					ret=m_enable_autoupdate;
				}
				return ret;
			}
			set
			{
				bool need_update=false;
				lock(SyncRoot)
				{
					if(m_enable_autoupdate!=value)
					{
						need_update=true;
						m_enable_autoupdate=value;
					}
				}
				
				if(need_update)
				{
					if(m_timer==null)
					{
						//если ещё не создан...
						//создаём таймер во временную ссылку
						Timer temp_timer=new Timer(new TimerCallback(auto_update_proc));
						//на тот случай, если другой поток сдуру не создал таймер одновременно
						//и атомарно заменяем постоянную ссылку на временную
						//если наш постоянный таймер ещё null,
						//а если уже не null - временная ссылка отправляется в мусор
						Interlocked.CompareExchange(ref m_timer,temp_timer,null);
						//после всего этого таймер безопасно должен быть создан
					}
					if(value)
					{
						//таймер надобно запустить
						TimeSpan my_period=AutoUpdatePeriod;
						m_timer.Change(my_period,my_period);
					}
					else
					{
						//останавливаем
						m_timer.Change(Timeout.Infinite,Timeout.Infinite);
					}
					
					On_property_changed("EnableAutoUpdate");
				}
			}
		}
		
		private TimeSpan m_autoupdate_period;
		public TimeSpan AutoUpdatePeriod
		{
			get
			{
				TimeSpan ret=TimeSpan.Zero;
				lock(SyncRoot)
				{
					ret=m_autoupdate_period;
				}
				return ret;
			}
			set
			{
				bool need_update=false;
				lock(SyncRoot)
				{
					if(m_autoupdate_period!=value)
					{
						m_autoupdate_period=value;
						need_update=true;
					}
				}
				
				if(need_update)
				{
					if(m_timer!=null)
					{
						m_timer.Change(value,value);
					}
					On_property_changed("AutoUpdatePeriod");
				}
			}
		}
		
		private int m_MaxEntriesPerUpdate=20;
		public int MaxEntriesPerUpdate
		{
			get
			{
				int ret=0;
				lock(SyncRoot)
				{
					ret=m_MaxEntriesPerUpdate;
				}
				return ret;
			}
			set
			{
				bool need_update=false;
				lock(SyncRoot)
				{
					if(m_MaxEntriesPerUpdate!=value)
					{
						m_MaxEntriesPerUpdate=value;
						need_update=true;
					}
				}
				
				if(need_update)
					On_property_changed("MaxEntriesPerUpdate");
			}
		}
		
		TimelineKind kind;
		public TimelineKind Kind {
			get
			{
				TimelineKind ret= TimelineKind.Home;
				lock(SyncRoot)
				{
					ret=kind;
				}
				return kind;
			}
			private set
			{
				bool need_update=false;
				lock(SyncRoot)
				{
					if(kind!=value)
					{
						kind = value;
						need_update=true;
					}
				}
				
				if(need_update)
				{
					On_property_changed("Kind");
					Clear();
				}
			}
		}
		
		private long? m_UserId=null;
		/// <summary>
		/// The ID of the user for whom to return results for.
		/// Helpful for disambiguating when a valid user ID is also a valid screen name.
		/// Ignored for home timeline
		/// </summary>
		public long? UserId
		{
			get
			{
				long? ret=null;
				lock(SyncRoot)
				{
					ret=m_UserId;
				}
				return ret;
			}
			set
			{
				bool need_update=false;
				lock(SyncRoot)
				{
					if(m_UserId!=value)
					{
						m_UserId=value;
						need_update=true;
					}
				}
				
				if(need_update)
				{
					On_property_changed("UserId");
					Clear();
				}
			}
		}
		
		private string m_UserScreenName=null;
		/// <summary>
		/// The screen name of the user for whom to return results for.
		/// Helpful for disambiguating when a valid screen name is also a user ID.
		/// Ignored for home  timeline
		/// </summary>
		public string UserScreenName
		{
			get
			{
				string ret=null;
				lock(SyncRoot)
				{
					ret=m_UserScreenName;
				}
				return ret;
			}
			set
			{
				bool need_update=false;
				lock(SyncRoot)
				{
					if(m_UserScreenName!=value)
					{
						m_UserScreenName=value;
						need_update=true;
					}
				}
				
				if(need_update)
				{
					On_property_changed("UserScreenName");
					Clear();
				}
			}
		}
		
		private bool m_TrimUser=false; //wa are always fetch user objects
		
		
		private bool m_IncludeRts=false;
		/// <summary>
		/// When set to false, the timeline will strip any native retweets
		/// (though they will still count toward both the maximal length
		/// of the timeline and the slice selected by the count parameter).
		/// Note: If you're using the trim_user parameter in conjunction
		/// with include_rts, the retweets will still contain a full user object.
		/// </summary>
		public bool IncludeRts
		{
			get
			{
				bool ret=false;
				lock(SyncRoot)
				{
					ret=m_IncludeRts;
				}
				return ret;
			}
			set
			{
				bool need_update=false;
				lock(SyncRoot)
				{
					if(m_IncludeRts!=value)
					{
						m_IncludeRts=value;
						need_update=true;
					}
				}
				
				if(need_update)
					On_property_changed("IncludeRts");
			}
		}
		
		
		private bool m_ExcludeReplies=false;
		/// <summary>
		/// This parameter will prevent replies from appearing in
		/// the returned timeline. Using exclude_replies with the count
		/// parameter will mean you will receive up-to count
		/// tweets — this is because the count parameter retrieves
		/// that many tweets before filtering out retweets and replies.
		/// </summary>
		public bool ExcludeReplies
		{
			get
			{
				bool ret=false;
				lock(SyncRoot)
				{
					ret=m_ExcludeReplies;
				}
				return ret;
			}
			set
			{
				bool need_update=false;
				lock(SyncRoot)
				{
					if(m_ExcludeReplies!=value)
					{
						m_ExcludeReplies=value;
						need_update=true;
					}
				}
				
				if(need_update)
					On_property_changed("ExcludeReplies");
			}
		}
		
		
		private bool m_ContributorDetails=false;
		/// <summary>
		/// This parameter enhances the contributors element
		/// of the status response to include the screen_name
		/// of the contributor.
		/// </summary>
		public bool ContributorDetails
		{
			get
			{
				bool ret=false;
				lock(SyncRoot)
				{
					ret=m_ContributorDetails;
				}
				return ret;
			}
			set
			{
				bool need_update=false;
				lock(SyncRoot)
				{
					if(m_ContributorDetails!=value)
					{
						m_ContributorDetails=value;
						need_update=true;
					}
				}
				
				if(need_update)
					On_property_changed("ContributorDetails");
			}
		}
		#endregion
		
		#region events
		
		public event EventHandler<TwObjectReceivedEventArgs<TwTweet>> TweetsReceived;
		
		private void on_tweets_received(TwObjectReceivedEventArgs<TwTweet> e)
		{
			if(TweetsReceived!=null)
			{
				TweetsReceived.Invoke(this,e);
			}
		}
		#endregion
		
		#region update engine
		private System.Threading.Timer m_timer;
		private const string PATH_FORMAT=@"{0}/{1}";
		private static string create_path(TimelineKind kind)
		{
			switch (kind)
			{
				case jellybird.core.TimelineKind.Home:
					return string.Format(PATH_FORMAT,Constants.STATUSES,Constants.HOME_TIMELINE);
				case jellybird.core.TimelineKind.Mentions:
					return string.Format(PATH_FORMAT,Constants.STATUSES,Constants.MENTIONS_TIMELINE);
				case jellybird.core.TimelineKind.User:
					return string.Format(PATH_FORMAT, Constants.STATUSES, Constants.USER_TIMELINE);
					/*
					 * в 1.1 этого нет
                case jellybird.core.TimelineKind.RetweetedByMe:
					return string.Format(PATH_FORMAT,Constants.STATUSES,Constants.RETWEETED_BY_ME);
				case jellybird.core.TimelineKind.RetweetedToMe:
					return string.Format(PATH_FORMAT,Constants.STATUSES,Constants.RETWEETED_TO_ME);
				case jellybird.core.TimelineKind.RetweetsOfMe:
					return string.Format(PATH_FORMAT,Constants.STATUSES,Constants.RETWEETS_OF_ME);
				case jellybird.core.TimelineKind.RetweetedToUser:
					return string.Format(PATH_FORMAT,Constants.STATUSES,Constants.RETWEETED_TO_USER);
				case jellybird.core.TimelineKind.RetweetedByUser:
					return string.Format(PATH_FORMAT,Constants.STATUSES,Constants.RETWEETED_BY_USER);
					 */
				default:
					throw new Exception("Invalid value for TimelineKind");
			}
		}
		
		private RestRequest create_request(int? count,long? since_id,long? max_id,int? page)
		{
			RestRequest rest_req=new RestRequest()
			{
				Method=WebMethod.Get,
				Path=create_path(Kind)
			};
			
			//объекты useer будем запрашивать всегда
			//и кэшировать их
			
			//добавляем параметры
			/*
			if((Kind==TimelineKind.User)||
			   (Kind== TimelineKind.RetweetedToUser)||
			   (Kind== TimelineKind.RetweetedByUser))
			 */
			if(kind==TimelineKind.User)
			{
				if(UserId.HasValue)
				{
					rest_req.AddParameter(Constants.USER_ID,UserId.ToString());
				}
				else if(!string.IsNullOrEmpty(UserScreenName))
				{
					rest_req.AddParameter(Constants.SCREEN_NAME,UserScreenName);
				}
				else
				{
					//попробуем пропустить без параметра
					//возможно прокатит для авторизованного юзера
					//throw new ArgumentException("Screen name or user id not specified.");
				}
			}
			
			rest_req.AddParameter(Constants.INCLUDE_ENTITIES,Constants.TRUE_STRING);
			
			if(since_id.HasValue)
				rest_req.AddParameter(Constants.SINCE_ID,since_id.ToString());
			if(count.HasValue)
				rest_req.AddParameter(Constants.COUNT,count.Value.ToString());
			if(max_id.HasValue)
				rest_req.AddParameter(Constants.MAX_ID,max_id.ToString());
			if(page.HasValue)
				rest_req.AddParameter(Constants.PAGE,page.ToString());
			if(m_TrimUser)
				rest_req.AddParameter(Constants.TRIM_USER,Constants.TRUE_STRING);
			if(IncludeRts)
				rest_req.AddParameter(Constants.INCLUDE_RTS,Constants.TRUE_STRING);
			if(ExcludeReplies)
				rest_req.AddParameter(Constants.EXCLUDE_REPLIES,Constants.TRUE_STRING);
			if(ContributorDetails)
				rest_req.AddParameter(Constants.CONTRIBUTORS_DETAILS,Constants.TRUE_STRING);
			
			return rest_req;
		}
		
		private void response_proc(RestRequest request, RestResponse response, object user_state)
		{
			#if DEBUG
			if(response!=null)
				Helper.OnDebugMessage
					(string.Format("Receive response from '{0}', Status={1}",response.ResponseUri,response.StatusCode));
			#endif
			
			try
			{
				if(response!=null)
				{

					if(response.StatusCode==System.Net.HttpStatusCode.OK)
					{
						try
						{
							List<TwTweet> received=new List<TwTweet>();
							if(response.ContentStream!=null)
							{
								#if DEBUG
								Helper.OnDebugMessage
									(string.Format("Read from response data. Length={0}.",response.ContentLength));
								#endif
								StreamReader reader=new StreamReader(response.ContentStream);
								
								JsonSerializer.Populate(reader,received);
								foreach(TwTweet tw in received)
								{
									tw.Client=Client;
									tw.Credentials=Credentials;
									//а то там будет ссылка на пустой клиент
									//и пустой credentials
									//а они нужны для кэширования user
									
									if(tw.RetweetedStatus!=null)
									{
										tw.RetweetedStatus.Client=Client;
										tw.RetweetedStatus.Credentials=Credentials;
									}
								}
								
								#if DEBUG
								Helper.OnDebugMessage
									(string.Format("Reads {0} tweets from response data.",received.Count));
								#endif
								
								//handling FirstReadedTweet
								if(Count!=0)
								{
									if(FirstReadedTweet==null)
										FirstReadedTweet=this[0];
								}
								
								//TODO: handling gaps needed
								List<TwTweet> really_added;
								AddRange(received,out really_added);
								on_tweets_received(new TwObjectReceivedEventArgs<TwTweet>(received,user_state));
								
								/*********************************************
								* handling gaps:
								* compare received tweets and really added
								* ********************************************/
								if(received.Count!=Count)
								{
									//отбрасываем случай, когда received - первая порция твитов
									//или когда с момента предыдущего обновления новых
									//твитов не добавлось
									
									//это последний добавленный в коллекцию твит
									TwTweet last_really_added=really_added.Count>0?really_added[really_added.Count-1]:null;
									
									if((last_really_added!=null)&&(received.Count!=0)&&(last_really_added.Id==received[received.Count-1].Id))
									{
										#if DEBUG
										Helper.OnDebugMessage(string.Format("Possible gap detected Id={0}",last_really_added.Id));
										#endif
										
										//самый последний тоже добавился
										//это означает, что перед ним возможен разрыв
//										last_really_added.GapDetected=true;
										this[last_really_added.Id].GapDetected=true;
									}
									//надо очистить GapDetected у принятых твитов кроме
									//кроме последнего
									for(int receive_ind=0;receive_ind<received.Count-1;receive_ind++)
									{
										TwTweet one_tweet=this.TryGet(received[receive_ind].Id);
										if(one_tweet!=null)
											one_tweet.GapDetected=false;
									}
								}
								//и надо бы добавить атрибут GapDetected последнему твиту во всей
								//коллекции, но только если received не пуст
								//ибо это означает, что уже извлечён
								//максимально низкий твит
								int this_count=Count;
								if(this_count!=0)
									this[this_count-1].GapDetected=received.Count!=0;
								/*************************************************/
								
								if(received.Count!=0)
									On_property_changed("CountOfUnreadedTweets");
							}
							else
							{
								//response stream null
								on_tweets_received
									(new TwObjectReceivedEventArgs<TwTweet>
									 (new NullReferenceException("No response data."),
									  user_state));
							}
						}
						catch(Exception ex)
						{
							//TODO сюда добавить человеческую обработку
							
							#if DEBUG
							Helper.OnDebugMessage
								(string.Format
								 ("Exception type={0} message={1}",ex.GetType(),ex.Message));
							#endif
							
							
							on_tweets_received(new TwObjectReceivedEventArgs<TwTweet>(ex,user_state));
						}
					}
					else
					{
						//response not OK
						on_tweets_received
							(new TwObjectReceivedEventArgs<TwTweet>(response.InnerException,user_state));
					}
				}
				else
				{
					//response null
					on_tweets_received
						(new TwObjectReceivedEventArgs<TwTweet>
						 (new NullReferenceException("No response."),
						  user_state));
				}
			}
			finally
			{
				if(response!=null)
					response.Dispose();
				
				On_end_update();
			}
		}
		
		private IAsyncResult begin_populate_intern
			(int? count,long? since_id,long? max_id,int? page, object user_state)
		{
			NeedChangeNotificationInvoke=true;
			
			On_begin_update();
			
			RestRequest rest_req=create_request
				(count,
				 since_id,
				 max_id,
				 page);
			
			#if DEBUG
			Helper.OnDebugMessage
				(string.Format("Begin async request '{0}'",rest_req.BuildEndpoint(Client)));
			#endif
			
			IAsyncResult res=Client.BeginRequest(rest_req,new RestCallback(response_proc),user_state);
			
			return res;
		}
		
		private void auto_update_proc(object state)
		{
			//вызывается таймером в потоке из пула
			
			//надо найти макимальное id уже полученное
			long? since_id=MaxKey-1L;
			if(since_id==-2)
				since_id=null;
			begin_populate_intern(MaxEntriesPerUpdate,since_id,null,null,null);
		}
		#endregion
		
		/// <summary>
		/// Update with rest api, all params are optional
		/// </summary>
		/// <param name="since_id">Returns results with an ID greater
		/// than (that is, more recent than) the specified ID.
		/// There are limits to the number of Tweets which can be
		/// accessed through the API. If the limit of Tweets has
		/// occured since the since_id, the since_id will be forced
		/// to the oldest ID available.</param>
		/// <param name="max_id">Returns results with an ID less
		/// than (that is, older than) or equal to the specified ID.</param>
		/// <param name="user_state">user object</param>
		/// <returns></returns>
		public IAsyncResult BeginUpdate
			(long? since_id=null,
			 long? max_id=null,
			 object user_state=null)
		{
			return begin_populate_intern
				(MaxEntriesPerUpdate,
				 since_id,
				 max_id,
				 null,
				 user_state);
		}
	}
	
	public class TwObjectReceivedEventArgs<T>:EventArgs
		where T:TwObjectBase
	{
		public TwObjectReceivedEventArgs(List<T> received,object user_state)
		{
			Succes=true;
			TweetsReceived=received;
			UserState=user_state;
		}
		
		public TwObjectReceivedEventArgs(Exception exception,object user_state)
		{
			Succes=false;
			Exception=exception;
			UserState=user_state;
		}
		
		public List<T> TweetsReceived { get; set; }
		public object UserState { get; set; }
		public bool Succes { get; set; }
		public Exception Exception { get; set; }
	}
	
	public enum TimelineKind
	{
		//Для api 1.1
		// https://dev.twitter.com/docs/api/1.1
		Home,
		Mentions,
		/*
		RetweetedByMe,
		RetweetedToMe,
		RetweetsOfMe,
		 */
		User,
		/*
		RetweetedToUser,
		RetweetedByUser
		 */
	}
}
